﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using ArduinoInterOp.Abstract;
using ArduinoInterOp.Delegates;
using ArduinoInterOp.SystemCommands;

namespace ArduinoInterOp
{
    // TODO: Add Log handler
    public class Arduino
    {
		#region Fields (8) 
        
        private readonly ArduinoListenerCommand _listenerCommand;
        private readonly IArduinoCommunication _communication;
        private readonly Dictionary<long, ArduinoQueuedOperation> _operationsIndexByOperationTicks;
        private static readonly ReaderWriterLock _operationsLock;
        private Thread _runnerThread;
        private bool _running;
        private bool _started;
        private readonly ArduinoSetup _setup;
        private const int TimeoutMillisecs = 100;
        private IList<ArduinoQueuedOperation> _commandsToRemove;
        #endregion Fields 

		#region Constructors (2) 

        public Arduino(ArduinoSetup arduinoSetup, IArduinoCommunication communication)
        {
            if (arduinoSetup == null) throw new ArgumentNullException("arduinoSetup");
            if (communication == null) throw new ArgumentNullException("communication");

            _commandsToRemove = new List<ArduinoQueuedOperation>();
            _communication = communication;
            _listenerCommand = new ArduinoListenerCommand(300);
            _setup = arduinoSetup;
            _operationsIndexByOperationTicks = new Dictionary<long, ArduinoQueuedOperation>();
            PinsManager = new ArduinoPinsManager(this, _setup);
            InitialzeCommunication();
            LoadSystemCommands();
        }

        private void LoadSystemCommands()
        {
            AddCommand(_listenerCommand);
        }

        static Arduino()
        {
            _operationsLock = new ReaderWriterLock();
        }

        internal ArduinoPinsManager PinsManager { get; set; }

        #endregion Constructors 

        public ArduinoPinValueData this[byte pinNumber]
        {
            get { return PinsManager.GetPinData(pinNumber); }
        }

		#region Methods (11) 

		// Public Methods (3) 

        public long AddCommand(ArduinoCommand command)
        {
            var operation = GetOperationFromCommand(command);

            try
            {
                _operationsLock.AcquireWriterLock(TimeoutMillisecs);

                //TODO: MakeCommand validation here!!!

                _operationsIndexByOperationTicks.Add(operation.OperationId, operation);
            }
            finally
            {
                _operationsLock.ReleaseWriterLock();
            }

            return operation.OperationId;
        }

        public void Runner()
        {
            while (_running)
            {
                Execute();
            }
        }



        public void Start()
        {
            if (!_started)
            {
                _started = true;
                var processSetup = false;
                var ackResponse = _communication.SendSyncedCommand("ack");

                if (string.IsNullOrEmpty(ackResponse))
                {
                    // TODO:
                    throw new Exception();
                }

                if (ackResponse.ToLowerInvariant() == "syn")
                {
                    if (_setup.Valid)
                    {
                        processSetup = true;
                    }
                    else
                    {
                        throw new Exception("Setup Not valid: " + _setup.ValidErrorMessage);
                    }
                }
                else if (ackResponse.Contains("-"))
                {
                    PreHandleCommandResponse(ackResponse, 1);
                }
                else
                {
                    // TODO:
                    throw new Exception();
                }

                ProcessSetup(processSetup);

                _communication.ReadyCommandsMode();

                CreaeteRunnerThreadAndRun();
            }
        }
		// Private Methods (7) 

        private void CommunicationResponse(object sender, ArsuinoResponseEventArgs response)
        {
            PreHandleCommandResponse(response.Response, response.Response.OperationExecutionTicks);
            HandleCommandResponse(response.Response);
        }

        private void HandleCommandResponse(ArduinoQueuedResponse response)
        {
            if (response == null) throw new ArgumentNullException("response");
            _operationsIndexByOperationTicks[response.OperationId].HandleResponse(/*response*/); // No need for response at this time
        }
        
        private void PreHandleCommandResponse(string responseString, long operationExecutionTicks)
        {
            var response = new ArduinoResponse();
            response.CreatePinDataFromString(responseString);

            PreHandleCommandResponse(response, operationExecutionTicks);
        }

        private void PreHandleCommandResponse(ArduinoResponse response, long operationExecutionTicks)
        {
            if (response == null) {throw new ArgumentNullException("response");}

            foreach (var operation in response.AllOperations)
            {
                switch (operation.CommandType)
                {
                    case ArduinoCommands.SetOutputPinValue:
                        PinsManager.UpdateOutputPin(operation.Pin, operation.Value, operationExecutionTicks);
                        break;
                    case ArduinoCommands.GetPinValue:
                        PinsManager.UpdateInputPin(operation.Pin, operation.Value, operationExecutionTicks);
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }       
            }
        }

        private void ProcessSetup(bool shouldInit)
        {
            if (shouldInit)
            {
                var response = _setup.Communicate(this._communication);

                PreHandleCommandResponse(response, 1);
            }

            // Initialize with preLoaded commands
            foreach (var preloadedArduinoCommand in _setup.PreloadedArduinoCommands)
            {
                AddCommand(preloadedArduinoCommand);
            }
        }

        private void CreaeteRunnerThreadAndRun()
        {
            _runnerThread = new Thread(Runner) {Name = "Runner Thread"};
            _runnerThread.Start();
            _running = true;
        }

        private void Execute()
        {
            try
            {
                _operationsLock.AcquireReaderLock(TimeoutMillisecs);
                var nowTicks = DateTime.Now.Ticks;

                //TODO: Maybe there's a better way
                foreach (var arduinoQueuedOperation in _commandsToRemove)
                {
                    _operationsIndexByOperationTicks.Remove(arduinoQueuedOperation.OperationId);
                }

                _commandsToRemove.Clear();

                foreach (var arduinoQueuedOperation in _operationsIndexByOperationTicks.Values)
                {
                    ProcessOperation(arduinoQueuedOperation, nowTicks);
                }
            }
            finally
            {
                _operationsLock.ReleaseReaderLock();
            }
        }

        public void SetPinValue(byte pinNumber, ushort value)
        {
            AddCommand(new ArduinoSetPinValueCommand() {Operations = new ArduinoPinOperationData[] {new ArduinoPinOperationData(ArduinoCommands.SetOutputPinValue, pinNumber, value), }});
        }

        private ArduinoQueuedOperation GetOperationFromCommand(ArduinoCommand command)
        {
            command.ArduinoHandler = this;
            return new ArduinoQueuedOperation(command);
        }

        private void InitialzeCommunication()
        {
            _communication.Response += CommunicationResponse;
        }

        private void ProcessOperation(ArduinoQueuedOperation arduinoQueuedOperation, long nowTicks)
        {
            switch (arduinoQueuedOperation.Status)
            {
                case ArduinoCommandState.Pending:
                    if (arduinoQueuedOperation.StartTimeInTicks < nowTicks)
                    {
                        arduinoQueuedOperation.ExecuteOperation(_communication);
                    }
                    break;
                case ArduinoCommandState.Stopped:
                    if (arduinoQueuedOperation.CheckIfStillShouldBeStopped())
                    {
                        ProcessOperation(arduinoQueuedOperation, nowTicks);
                    }
                    break;
                case ArduinoCommandState.InProgress:
                    break;
                case ArduinoCommandState.Invalid:
                case ArduinoCommandState.Finished:
                    _commandsToRemove.Add(arduinoQueuedOperation);
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }

        #endregion Methods 

        public static void DebugMessage(string message)
        {
            Debug.WriteLine("------------------------------ " + message);
        }

        internal void AddListenerToPin(byte pinNumber)
        {
            this._listenerCommand.AddPinListener(pinNumber);
        }

        internal void RemoveListenerToPin(byte pinNumber)
        {
            this._listenerCommand.RemovePinListener(pinNumber);
        }
    }
}